#!/usr/bin/env python3

# Copyright (c) 2020-2021 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


import os
import re
import pathlib
from ._util import Util
from ._handy import Handy
from ._po import KnownHostInfo


class ClientSysCfg:

    def __init__(self, prefix="/", cfg_dir=None, read_only=False):
        self._prefix = prefix

        if cfg_dir is None:
            self._cfgDir = os.path.join(self._prefix, "etc", "ssh")
        else:
            assert not cfg_dir.startswith("/")
            self._cfgDir = os.path.join(self._prefix, cfg_dir)

        self._readOnly = read_only

        try:
            buf = pathlib.Path(self._getCfgFilePath()).read_text()
            self._cfgLines = Util.readListBuffer(buf)
            if not self._readOnly and "\n".join(self._cfgLines) != buf:
                self._writeSysCfgFile()
        except FileNotFoundError:
            self._cfgLines = []
            if not self._readOnly:
                self._writeSysCfgFile()

    @property
    def paths(self):
        return [
            self._cfgDir,
            self._getCfgFilePath(),
        ]

    @property
    def cfg_file_path(self):
        return self._getCfgFilePath()

    def get_global_known_hosts(self):
        self._assertGlobalKnownHostsFile()

        try:
            return [KnownHostInfo(line=x) for x in Util.readListFile(self._getGlobalKnownHostsFilePath())]
        except FileNotFoundError:
            return []

    def add_or_update_global_known_host(self, host, key_type, key):
        self._assertGlobalKnownHostsFile()

        try:
            lineList = Util.readListFile(self._getGlobalKnownHostsFilePath())
        except FileNotFoundError:
            lineList = []

        bFound = False
        for line in lineList:
            obj = KnownHostInfo(line=line)
            if obj.host == host and obj.key_type == key_type and obj.key == key:
                bFound = True

        if not bFound:
            lineList.append(KnownHostInfo(host=host, key_type=key_type, key=key)._toLine())
            self._writeGlobalKnownHostsFile(lineList)

    def is_using_user_known_hosts(self):
        return Handy.lineListReSearch(self._cfgLines, r"^UserKnownHostsFile\s+/dev/null$") is None

    def set_using_user_known_hosts(self, value):
        assert not self._readOnly

        dst = "UserKnownHostsFile /dev/null" if not value else None
        if Handy.lineListReplaceOrAddLine(self._cfgLines, r"^UserKnownHostsFile\s+", dst):
            self._writeSysCfgFile()

    def get_sent_environment_variables(self):
        return [m.group(1) for m in Handy.lineListReSearchMulti(self._cfgLines, r"^SendEnv\s+(\S+)$")]

    def send_common_environment_variables(self):
        assert not self._readOnly

        bModified = False
        for env in Handy.COMMON_ENV_LIST:
            if not Handy.lineListReSearch(self._cfgLines, f"^SendEnv\\s+{re.escape(env)}$") is not None:
                self._cfgLines.append(f"SendEnv {env}")
                bModified = True
        if bModified:
            self._writeSysCfgFile()

    def send_environment_variable(self, env):
        assert not self._readOnly

        if not Handy.lineListReSearch(self._cfgLines, f"^SendEnv\\s+{re.escape(env)}$") is not None:
            self._cfgLines.append(f"SendEnv {env}")
            self._writeSysCfgFile()

    def check(self, auto_fix=False, error_callback=None):
        if auto_fix:
            assert not self._readOnly

        if error_callback is None:
            error_callback = Util.doNothing

        # check global known_hosts file
        hostSet = set()
        for obj in self.get_global_known_hosts():
            for h in hostSet:
                if obj.host == h:
                    error_callback('duplicate host found in "%s"' % (self._getGlobalKnownHostsFilePath()))
            hostSet.add((obj.key_type, obj.key))

    def _getCfgFilePath(self):
        return os.path.join(self._cfgDir, "ssh_config")

    def _writeSysCfgFile(self):
        os.makedirs(self._cfgDir, exist_ok=True)
        pathlib.Path(self._getCfgFilePath()).write_text("\n".join(self._cfgLines) + "\n")

    def _assertGlobalKnownHostsFile(self):
        if not self._cfgDir.endswith("/etc/ssh"):
            fullfn = os.path.join("/" + os.path.relpath(self._cfgDir, self._prefix), "ssh_known_hosts")
            assert Handy.lineListReSearch(self._cfgLines, f"^GlobalKnownHostsFile\\s+{re.escape(fullfn)}$") is not None
        else:
            assert Handy.lineListReSearch(self._cfgLines, "^GlobalKnownHostsFile\\s+") is None

    def _getGlobalKnownHostsFilePath(self):
        return os.path.join(self._cfgDir, "ssh_known_hosts")

    def _writeGlobalKnownHostsFile(self, lineList):
        os.makedirs(self._cfgDir, exist_ok=True)
        pathlib.Path(self._getGlobalKnownHostsFilePath()).write_text("\n".join(lineList) + "\n")
